Skip to content

[INF-5286] Write reconciliation functions that pave over inconsistencies in API responses#233

Open
martin-ably wants to merge 1 commit intomainfrom
martin/qos/write-reconciliation-function
Open

[INF-5286] Write reconciliation functions that pave over inconsistencies in API responses#233
martin-ably wants to merge 1 commit intomainfrom
martin/qos/write-reconciliation-function

Conversation

@martin-ably
Copy link
Copy Markdown
Collaborator

@martin-ably martin-ably commented Apr 13, 2026

This should result in the TF provider being a little more resistant to upstream field changes.

Additionally, adds tests that check if any unexpected fields are returned when a minimal (required-only) config is sent.

Summary by CodeRabbit

  • Tests

    • Added many minimal acceptance tests across resource types validating lightweight configs and plan-only stability.
    • Added unit tests covering detailed reconciliation behavior between configured and API-returned values.
  • Refactor

    • Centralized reconciliation layer to standardize merging of plan/state and API responses across create/read/update flows, including read-mode handling for computed fields.
  • Chores

    • Added helper utilities to support tests and reconciliation; extended test runner timeout in CI.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 13, 2026

Warning

Rate limit exceeded

@martin-ably has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 35 minutes and 14 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 35 minutes and 14 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 282181f6-74a1-450d-b67d-7757418f44dd

📥 Commits

Reviewing files that changed from the base of the PR and between 9ccd17e and 787fffc.

📒 Files selected for processing (31)
  • .github/workflows/check.yml
  • internal/provider/ingress_rules.go
  • internal/provider/modifiers.go
  • internal/provider/provider_test.go
  • internal/provider/reconcile.go
  • internal/provider/reconcile_test.go
  • internal/provider/resource_ably_app.go
  • internal/provider/resource_ably_app_test.go
  • internal/provider/resource_ably_ingress_rule_mongo_test.go
  • internal/provider/resource_ably_ingress_rule_postgres_outbox_test.go
  • internal/provider/resource_ably_key.go
  • internal/provider/resource_ably_key_test.go
  • internal/provider/resource_ably_namespace.go
  • internal/provider/resource_ably_namespace_test.go
  • internal/provider/resource_ably_queue.go
  • internal/provider/resource_ably_queue_test.go
  • internal/provider/resource_ably_rule_amqp_external_test.go
  • internal/provider/resource_ably_rule_amqp_test.go
  • internal/provider/resource_ably_rule_azure_function_test.go
  • internal/provider/resource_ably_rule_http_cloudflare_worker_test.go
  • internal/provider/resource_ably_rule_http_google_cloud_function_test.go
  • internal/provider/resource_ably_rule_http_test.go
  • internal/provider/resource_ably_rule_ifttt_test.go
  • internal/provider/resource_ably_rule_kafka_test.go
  • internal/provider/resource_ably_rule_kinesis_test.go
  • internal/provider/resource_ably_rule_lambda_test.go
  • internal/provider/resource_ably_rule_pulsar_test.go
  • internal/provider/resource_ably_rule_sqs_test.go
  • internal/provider/resource_ably_rule_zapier_test.go
  • internal/provider/rules.go
  • internal/provider/rules_unit_test.go

Walkthrough

Adds a reconciler to merge Terraform plan/state with Ably API responses, extensive unit tests for reconciliation logic, refactors many resource CRUD mappings to use the reconciler, introduces shared minimal HCL test helpers, and adds numerous minimal acceptance tests plus a small CI timeout tweak.

Changes

Cohort / File(s) Summary
Test helpers & CI
internal/provider/provider_test.go, .github/workflows/check.yml
Added tfProvider constant and minimalRuleConfig / minimalIngressRuleConfig helpers for acceptance tests; increased go test timeout to 20m in CI.
Reconciler core + tests
internal/provider/reconcile.go, internal/provider/reconcile_test.go
New reconciler type and reconciliation functions for strings/bools/ints/slices/maps, plan accessors, and converters; comprehensive unit tests exercising input/API/computed matrices.
Rules & ingress mapping refactor
internal/provider/rules.go, internal/provider/ingress_rules.go
Switched rule and ingress response mapping to use the reconciler (new signatures include reading bool or *reconciler) and reconciler-aware AWS auth handling.
Resource state builders
internal/provider/resource_ably_app.go, internal/provider/resource_ably_key.go, internal/provider/resource_ably_namespace.go, internal/provider/resource_ably_queue.go
Added build*State helpers and refactored Create/Read/Update to construct reconciler, build reconciled state, and short-circuit on diagnostics.
Unit test updates
internal/provider/rules_unit_test.go
Updated tests to construct a reconciler in read mode and assert diagnostics are empty before validating fields.
Many minimal acceptance tests — rules & resources
internal/provider/resource_ably_app_test.go, internal/provider/resource_ably_key_test.go, internal/provider/resource_ably_namespace_test.go, internal/provider/resource_ably_queue_test.go, internal/provider/resource_ably_ingress_rule_mongo_test.go, internal/provider/resource_ably_ingress_rule_postgres_outbox_test.go, internal/provider/resource_ably_rule_amqp_test.go, internal/provider/resource_ably_rule_amqp_external_test.go, internal/provider/resource_ably_rule_azure_function_test.go, internal/provider/resource_ably_rule_http_test.go, internal/provider/resource_ably_rule_http_cloudflare_worker_test.go, internal/provider/resource_ably_rule_http_google_cloud_function_test.go, internal/provider/resource_ably_rule_ifttt_test.go, internal/provider/resource_ably_rule_kafka_test.go, internal/provider/resource_ably_rule_kinesis_test.go, internal/provider/resource_ably_rule_lambda_test.go, internal/provider/resource_ably_rule_pulsar_test.go, internal/provider/resource_ably_rule_sqs_test.go, internal/provider/resource_ably_rule_zapier_test.go
Added many minimal acceptance tests that provision minimal resource configs, assert presence of id and key computed/default fields (e.g., status, request_mode), and include PlanOnly steps to validate planning stability.

Sequence Diagram(s)

sequenceDiagram
    participant Terraform as Terraform (plan/state)
    participant Provider as Provider (reconciler)
    participant API as Ably API
    participant State as Terraform State

    Terraform->>Provider: Create/Read/Update with plan/state
    Provider->>API: Call Ably API (Create/Read/Update)
    API-->>Provider: API response (resource fields)
    Provider->>Provider: reconciler compares plan vs API (str/bool/int/slice/map)
    Provider-->>State: Set reconciled Terraform types and diagnostics
    State-->>Terraform: Apply/Plan result (includes computed values)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop through plans and API replies,
Reconciling strings, booleans, and ties.
Minimal tests bloom, one tiny seed,
Diagnostics gathered, state takes the lead.
A whiskered helper keeps everything neat. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main objective of the PR: introducing reconciliation functions to handle API response inconsistencies, which aligns with the primary changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch martin/qos/write-reconciliation-function

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@martin-ably martin-ably changed the title [INF-5286] Write a reconciliation function that paves over inconsistencies in API responses [INF-5286] Write reconciliation functions that pave over inconsistencies in API responses Apr 13, 2026
coderabbitai[bot]

This comment was marked as resolved.

Comment thread internal/provider/reconcile.go
@martin-ably martin-ably force-pushed the martin/qos/write-reconciliation-function branch from 5db2c4a to 4d0f9c7 Compare April 13, 2026 13:42
coderabbitai[bot]

This comment was marked as resolved.

@martin-ably martin-ably force-pushed the martin/qos/write-reconciliation-function branch 2 times, most recently from 6955671 to dbaf109 Compare April 13, 2026 14:14
@martin-ably martin-ably requested a review from surminus April 13, 2026 14:27
@martin-ably
Copy link
Copy Markdown
Collaborator Author

martin-ably commented Apr 13, 2026

I've tested this version against my account in production, and it's creating the apps correctly, as well as the keys. So if this change doesn't work in the main production account, then the account itself might be having some issues.

// buildQueueState reconciles plan/state input with an API response.
func buildQueueState(rc *reconciler, input AblyQueue, api control.QueueResponse) AblyQueue {
return AblyQueue{
AppID: rc.str("app_id", input.AppID, types.StringValue(api.AppID), false),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than have to pass in a boolean value for whether it's computed or not, I wonder whether we could have a buildState() method on the resource type itself, which could lookup the field? But perhaps maybe that's something for a future simplification.

@surminus
Copy link
Copy Markdown
Contributor

I think this looks fine, although not applied to all resources. Is the plan to fix the broken resources only in this PR?

@martin-ably martin-ably force-pushed the martin/qos/write-reconciliation-function branch from dbaf109 to 9ccd17e Compare April 14, 2026 08:17
@martin-ably
Copy link
Copy Markdown
Collaborator Author

I was originally just patching up the main offenders, but I've added the remaining reconciliation code for the rules. I've had to implement additional functions to handle maps, slices, and planning field accessors, as the rules have more finicky logic (as they're all variants over generics).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
internal/provider/reconcile.go (1)

196-199: ⚠️ Potential issue | 🟠 Major

Redact API strings from reconciliation errors.

output.ValueString() can be a secret (key, secret_access_key, tokens, header values, etc.). Emitting it in diagnostics leaks remote data into provider logs.

💡 Suggested fix
-		return types.StringNull(), fmt.Errorf(
-			"reconcile %q: API returned %q but field was not set in config and is not computed",
-			field, output.ValueString(),
-		)
+		return types.StringNull(), fmt.Errorf(
+			"reconcile %q: API returned a value but field was not set in config and is not computed",
+			field,
+		)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/provider/reconcile.go` around lines 196 - 199, The reconciliation
error is leaking API-returned secret strings by interpolating
output.ValueString() into the error; change the error construction in the
reconcile code to avoid including the raw output value and instead emit a
redacted placeholder (e.g. "<redacted>" or indicate presence/length) while
preserving field context and the fact it was unexpected; update the return that
currently references output.ValueString() to use the redacted token and keep the
rest of the message and types.StringNull() behavior unchanged so callers can
still detect the reconcile failure without exposing secrets.
internal/provider/resource_ably_namespace.go (1)

174-181: ⚠️ Potential issue | 🟠 Major

Preserve batching/conflation values when the enabled flag is omitted.

If api.BatchingEnabled or api.ConflationEnabled is nil, these branches skip reconciliation entirely and leave batching_interval, conflation_interval, and conflation_key unset. A partial API response will therefore clear previously configured values and reintroduce drift on the exact payloads this reconciler is supposed to absorb. Treat nil as “field omitted” and only clear these attributes when the API explicitly returns false.

💡 Suggested fix
-	if api.BatchingEnabled != nil && *api.BatchingEnabled {
-		ns.BatchingInterval = rc.int64val("batching_interval", input.BatchingInterval, optIntValue(api.BatchingInterval), true)
-	}
-
-	if api.ConflationEnabled != nil && *api.ConflationEnabled {
-		ns.ConflationInterval = rc.int64val("conflation_interval", input.ConflationInterval, optIntValue(api.ConflationInterval), true)
-		ns.ConflationKey = rc.str("conflation_key", input.ConflationKey, optStringValue(api.ConflationKey), true)
-	}
+	switch {
+	case api.BatchingEnabled == nil:
+		ns.BatchingInterval = input.BatchingInterval
+	case *api.BatchingEnabled:
+		ns.BatchingInterval = rc.int64val("batching_interval", input.BatchingInterval, optIntValue(api.BatchingInterval), true)
+	default:
+		ns.BatchingInterval = types.Int64Null()
+	}
+
+	switch {
+	case api.ConflationEnabled == nil:
+		ns.ConflationInterval = input.ConflationInterval
+		ns.ConflationKey = input.ConflationKey
+	case *api.ConflationEnabled:
+		ns.ConflationInterval = rc.int64val("conflation_interval", input.ConflationInterval, optIntValue(api.ConflationInterval), true)
+		ns.ConflationKey = rc.str("conflation_key", input.ConflationKey, optStringValue(api.ConflationKey), true)
+	default:
+		ns.ConflationInterval = types.Int64Null()
+		ns.ConflationKey = types.StringNull()
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/provider/resource_ably_namespace.go` around lines 174 - 181, The
reconciler currently treats a nil enabled flag as "disabled" because it only
sets batching/conflation when api.BatchingEnabled or api.ConflationEnabled is
true, which causes omitted fields to be cleared; change the logic so nil means
"omit/do nothing" and only act when the pointer is non-nil: for
api.BatchingEnabled, if api.BatchingEnabled == nil do nothing, else if
*api.BatchingEnabled set ns.BatchingInterval via rc.int64val(...), otherwise
explicitly clear ns.BatchingInterval; similarly for api.ConflationEnabled, if
nil do nothing, else if true set ns.ConflationInterval and ns.ConflationKey
using rc.int64val/rc.str with
input.BatchingInterval/input.ConflationInterval/input.ConflationKey and
optIntValue/optStringValue, and if false explicitly clear ns.ConflationInterval
and ns.ConflationKey.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/provider/resource_ably_app_test.go`:
- Around line 255-258: The test assertion for the default of the tls_only
attribute is inverted: update the TestCheckResourceAttr call in
resource_ably_app_test.go that references "ably_app.app0", "tls_only" to expect
"false" (matching the schema default in resource_ably_app.go for tls_only);
leave the other defaults (e.g., "status" and "apns_use_sandbox_endpoint")
unchanged.

In `@internal/provider/resource_ably_rule_pulsar_test.go`:
- Around line 110-118: The test config variable created via minimalRuleConfig
includes a JWT-looking literal in the Pulsar target's authentication.token;
replace that hardcoded JWT-like string with a clearly fake placeholder or
generated dummy (e.g., "fake-token-placeholder" or a call that generates a
random string) so scanners won't flag it and tests remain safe. Update the token
value inside the config passed to minimalRuleConfig (the pulsar target's
authentication.token) and ensure any assertions that depend on the token are
adjusted to expect the placeholder or generated value.

In `@internal/provider/rules.go`:
- Around line 342-364: When rc.reading is true and the API-reported
auth.AuthenticationMode differs from planAuth.AuthenticationMode, force the
inactive-mode plan inputs to null so rc.str won't echo stale values: detect the
mismatch after setting modeOutput and before calling rc.str, and for the
inactive branch set the corresponding planAuth fields (e.g.,
planAuth.AccessKeyId / planAuth.SecretAccessKey for AWSAuthModeAssumeRole, or
planAuth.RoleArn for AWSAuthModeCredentials) to types.StringNull() (or pass a
null-valued variable) so rc.str("target.authentication.*", ...) receives a null
plan for those inactive fields; keep using
modeOutput/keyOutput/secretOutput/arnOutput as you already compute them and
return AwsAuth with rc.str calls unchanged.

---

Duplicate comments:
In `@internal/provider/reconcile.go`:
- Around line 196-199: The reconciliation error is leaking API-returned secret
strings by interpolating output.ValueString() into the error; change the error
construction in the reconcile code to avoid including the raw output value and
instead emit a redacted placeholder (e.g. "<redacted>" or indicate
presence/length) while preserving field context and the fact it was unexpected;
update the return that currently references output.ValueString() to use the
redacted token and keep the rest of the message and types.StringNull() behavior
unchanged so callers can still detect the reconcile failure without exposing
secrets.

In `@internal/provider/resource_ably_namespace.go`:
- Around line 174-181: The reconciler currently treats a nil enabled flag as
"disabled" because it only sets batching/conflation when api.BatchingEnabled or
api.ConflationEnabled is true, which causes omitted fields to be cleared; change
the logic so nil means "omit/do nothing" and only act when the pointer is
non-nil: for api.BatchingEnabled, if api.BatchingEnabled == nil do nothing, else
if *api.BatchingEnabled set ns.BatchingInterval via rc.int64val(...), otherwise
explicitly clear ns.BatchingInterval; similarly for api.ConflationEnabled, if
nil do nothing, else if true set ns.ConflationInterval and ns.ConflationKey
using rc.int64val/rc.str with
input.BatchingInterval/input.ConflationInterval/input.ConflationKey and
optIntValue/optStringValue, and if false explicitly clear ns.ConflationInterval
and ns.ConflationKey.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1cb81c6-3ca1-4700-859a-3f8ef7127f32

📥 Commits

Reviewing files that changed from the base of the PR and between 4d0f9c7 and 9ccd17e.

📒 Files selected for processing (30)
  • .github/workflows/check.yml
  • internal/provider/ingress_rules.go
  • internal/provider/provider_test.go
  • internal/provider/reconcile.go
  • internal/provider/reconcile_test.go
  • internal/provider/resource_ably_app.go
  • internal/provider/resource_ably_app_test.go
  • internal/provider/resource_ably_ingress_rule_mongo_test.go
  • internal/provider/resource_ably_ingress_rule_postgres_outbox_test.go
  • internal/provider/resource_ably_key.go
  • internal/provider/resource_ably_key_test.go
  • internal/provider/resource_ably_namespace.go
  • internal/provider/resource_ably_namespace_test.go
  • internal/provider/resource_ably_queue.go
  • internal/provider/resource_ably_queue_test.go
  • internal/provider/resource_ably_rule_amqp_external_test.go
  • internal/provider/resource_ably_rule_amqp_test.go
  • internal/provider/resource_ably_rule_azure_function_test.go
  • internal/provider/resource_ably_rule_http_cloudflare_worker_test.go
  • internal/provider/resource_ably_rule_http_google_cloud_function_test.go
  • internal/provider/resource_ably_rule_http_test.go
  • internal/provider/resource_ably_rule_ifttt_test.go
  • internal/provider/resource_ably_rule_kafka_test.go
  • internal/provider/resource_ably_rule_kinesis_test.go
  • internal/provider/resource_ably_rule_lambda_test.go
  • internal/provider/resource_ably_rule_pulsar_test.go
  • internal/provider/resource_ably_rule_sqs_test.go
  • internal/provider/resource_ably_rule_zapier_test.go
  • internal/provider/rules.go
  • internal/provider/rules_unit_test.go
✅ Files skipped from review due to trivial changes (4)
  • .github/workflows/check.yml
  • internal/provider/resource_ably_ingress_rule_mongo_test.go
  • internal/provider/resource_ably_rule_amqp_test.go
  • internal/provider/resource_ably_rule_ifttt_test.go
🚧 Files skipped from review as they are similar to previous changes (16)
  • internal/provider/resource_ably_rule_http_test.go
  • internal/provider/resource_ably_rule_http_cloudflare_worker_test.go
  • internal/provider/resource_ably_rule_lambda_test.go
  • internal/provider/resource_ably_queue_test.go
  • internal/provider/resource_ably_rule_http_google_cloud_function_test.go
  • internal/provider/resource_ably_rule_amqp_external_test.go
  • internal/provider/resource_ably_rule_kafka_test.go
  • internal/provider/resource_ably_rule_sqs_test.go
  • internal/provider/resource_ably_rule_azure_function_test.go
  • internal/provider/resource_ably_rule_zapier_test.go
  • internal/provider/resource_ably_rule_kinesis_test.go
  • internal/provider/resource_ably_namespace_test.go
  • internal/provider/ingress_rules.go
  • internal/provider/resource_ably_ingress_rule_postgres_outbox_test.go
  • internal/provider/resource_ably_app.go
  • internal/provider/provider_test.go

Comment thread internal/provider/resource_ably_app_test.go
Comment thread internal/provider/resource_ably_rule_pulsar_test.go
Comment thread internal/provider/rules.go
…I responses

This should result in the TF provider being a little more resistant to
upstream field changes.
@martin-ably martin-ably force-pushed the martin/qos/write-reconciliation-function branch from 9ccd17e to 787fffc Compare April 14, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants